using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows;
using System.Diagnostics;
using System.Threading;
using System.ComponentModel;
using System.Windows.Media.Imaging;
using System.Data.SQLite;
using System.Windows.Media;
using System.Xml.Linq;
using System.Net;
using System.Xml;
using Microsoft.Win32;

namespace KeyboardImageViewer
{
	public class ImageManager
	{
		private const string ConfigFileName = "KeyboardImageViewer.txt";
        private const int NumImagesToCacheAhead = 1;
        private const int CacheCurrentIndex = NumImagesToCacheAhead;
        private const int FileChunkSize = 10;
        private const int HashChunkSize = 50;
        private const int SummaryChunkSize = 1;
        private const int HashVisibleThreshold = 100;
        private const int SummaryVisibleThreshold = 1;

        private string[] extensions = new string[] { "jpg", "jpeg", "gif", "png", "bmp", "tif" };

		private MainWindow mainForm;
        private ViewerDatabaseConfig databaseConfig;

        private SQLiteConnection connection;

#if !DEBUG
        private BackgroundWorker updateDownloader;
        private bool processDownloadsUpdates = true;
#endif

        private ImageQuery imageQuery;
        private List<int> imageList;

        private int currentListId;

        private CachedImage[] imageCache;
        private int imageListVersion;
        private bool clearCacheWhenLoadCompletes;

        private Queue<IProcessTask> imagesToProcess;
        private int chunkSize;
        private object imagesToProcessLock = new object();
        private int chunksCompleted;
        private int numBackgroundWorkers;
        private int workersCompleted;
        private bool showingProgress;
        private int numImagesToProcess;

        private List<string> movedDirectoriesToProcess;

		private int currentIndex = -1;

		public ImageManager(MainWindow mainForm)
		{
			this.mainForm = mainForm;

            this.imagesToProcess = new Queue<IProcessTask>();
            this.imageQuery = new ImageQuery();
            this.imageCache = new CachedImage[NumImagesToCacheAhead * 2 + 1];

            for (int i = 0; i < this.imageCache.Length; i++)
            {
                this.imageCache[i] = new CachedImage();
            }

            bool newDataFile = !File.Exists(Database.DatabaseFile);
            bool oldConfigFound = false;

            if (newDataFile)
            {
                DirectorySet sourceDirectories = null;

                Welcome welcomeScreen = new Welcome();
                if ((bool)welcomeScreen.ShowDialog())
                {
                    sourceDirectories = welcomeScreen.SourceDirectories;
                }
                else
                {
                    Environment.Exit(0);
                }

                this.connection = new SQLiteConnection(Database.ConnectionString);
                this.connection.Open();

                foreach (BaseDirectory baseDirectory in sourceDirectories.BaseDirectories)
                {
                    if (File.Exists(Path.Combine(baseDirectory.Path, ConfigFileName)))
                    {
                        // Create tables at DB version 2
                        Database.CreateTables(this.connection, false);

                        ViewerConfig oldConfig = ViewerConfig.FromFiles(Path.Combine(baseDirectory.Path, ConfigFileName));

                        Database.PopulateTablesFromOldConfig(this.connection, oldConfig, baseDirectory.Path);

                        oldConfigFound = true;
                        break;
                    }
                }

                if (!oldConfigFound)
                {
                    Database.CreateTables(this.connection);

                    Database.PopulateDefaultConfig(this.connection, sourceDirectories);
                }
            }

            if (this.connection == null)
            {
                this.connection = new SQLiteConnection(Database.ConnectionString);
                this.connection.Open();
            }

            this.HandlePendingUpdate(this.connection);

            if (!newDataFile || oldConfigFound)
            {
                DatabaseUpgrades.UpgradeIfNeeded(this.connection);
            }

            this.databaseConfig = new ViewerDatabaseConfig(this.connection);

            // Now that configuration has been initialized, check our update status.
            this.CheckUpdates();

            this.databaseConfig.ApplyWindowSize("Main", this.mainForm);

            this.SetImageList(this.databaseConfig.StartingListId);

			this.RefreshFiles();
		}

		#region Public Properties

		public bool ImageShowing
		{
			get
			{
                return this.imageList.Count > 0 && this.imageCache[CacheCurrentIndex].Image != null;
			}
		}

        public bool ImageLoading
        {
            get
            {
                return this.imageList.Count > 0 && this.imageCache[CacheCurrentIndex].CallInProgress;
            }
        }

        public ImageUserData CurrentImageData
        {
            get
            {
                return Database.GetImageUserData(this.connection, this.CurrentImageId);
            }
        }

        public SQLiteConnection Connection
        {
            get
            {
                return this.connection;
            }
        }

        public int CurrentListId
        {
            get
            {
                return this.currentListId;
            }
        }

        public string CurrentImageName
        {
            get
            {
                SQLiteCommand getName = new SQLiteCommand("SELECT name FROM images WHERE id = " + this.CurrentImageId, this.connection);
                using (SQLiteDataReader reader = getName.ExecuteReader())
                {
                    reader.Read();

                    return reader.GetString("name");
                }
            }

            set
            {
                string oldName = this.CurrentImageName;
                string newNameWithDir = Path.Combine(Path.GetDirectoryName(oldName), value);

                try
                {
                    File.Move(oldName, newNameWithDir);

                    Database.SetImageName(this.connection, this.CurrentImageId, newNameWithDir);

                    this.mainForm.UpdateInfoBar(this);
                }
                catch (IOException)
                {
                    HandleMoveException();
                }
                catch (UnauthorizedAccessException)
                {
                    HandleMoveException();
                }
            }
        }

        public int CurrentImageId
        {
            get
            {
                return this.imageList[this.currentIndex];
            }
        }

		public bool SearchFileName
		{
			get { return this.databaseConfig.SearchFileName; }
		}

		public bool SearchTags
		{
            get { return this.databaseConfig.SearchTags; }
		}

		public string Search
		{
            get { return this.imageQuery.Search; }
		}

        public List<string> CurrentTags
        {
            get
            {
                return Database.GetTags(this.connection, this.CurrentImageId);
            }

            set
            {
                Database.SetTags(this.connection, value, this.CurrentImageId, this.databaseConfig.TagDependencies);

                this.mainForm.UpdateInfoBar(this);
            }
        }

        public List<KeyValuePair<string, string>> DependencyList
        {
            get
            {
                return this.databaseConfig.TagDependencies;
            }

            set
            {
                this.databaseConfig.TagDependencies = value;

                Database.RebuildDependentTags(this.connection, value);
            }
        }

        public ViewerDatabaseConfig Config
		{
			get { return this.databaseConfig; }
		}

        public List<int> ImageList
        {
            get { return this.imageList; }
        }

        public ImageQuery ImageQuery
        {
            get { return this.imageQuery; }
        }

		public int CurrentIndex
		{
			get { return this.currentIndex; }
		}

		public int TotalImages
		{
			get { return this.imageList.Count; }
		}

		#endregion

		private ViewerImage CurrentImage
		{
            get
            {
                return this.imageCache[CacheCurrentIndex].Image;
            }
			set
			{
                this.imageCache[CacheCurrentIndex].Image = value;
			}
		}

		#region Public Methods

        public void MoveLeft()
        {
            if (this.ImageShowing && this.currentIndex > 0)
            {
                this.currentIndex--;

                this.mainForm.RemoveFromCache(this.imageCache[this.imageCache.Length - 1].UIImage);

                // Shift image cache and call progress
                for (int i = this.imageCache.Length - 2; i >= 0; i--)
                {
                    this.imageCache[i + 1] = this.imageCache[i];
                }

                this.imageCache[0] = new CachedImage();

                // If we have an image cached, display it.
                if (this.imageCache[CacheCurrentIndex].Image != null)
                {
                    this.mainForm.SetPicture(this.imageCache[CacheCurrentIndex]);

                    // Load more images if we need to.
                    this.StartLoadingAdditionalImages();
                }
                else
                {
                    // If we don't, start loading the image, if it's not loading already.
                    this.mainForm.SetLoadingPicture();

                    if (!this.imageCache[CacheCurrentIndex].CallInProgress)
                    {
                        this.LoadImage(this.currentIndex);
                    }
                }
            }
        }

        public void MoveRight()
        {
            if (this.ImageShowing && this.currentIndex < this.imageList.Count - 1)
            {
                this.currentIndex++;

                this.mainForm.RemoveFromCache(this.imageCache[0].UIImage);

                // Shift image cache and call progress
                for (int i = 0; i < this.imageCache.Length - 1; i++)
                {
                    this.imageCache[i] = this.imageCache[i + 1];
                }

                this.imageCache[this.imageCache.Length - 1] = new CachedImage();

                // If we have an image cached, display it.
                if (this.imageCache[CacheCurrentIndex].Image != null)
                {
                    this.mainForm.SetPicture(this.imageCache[CacheCurrentIndex]);

                    // Load more images if we need to.
                    this.StartLoadingAdditionalImages();
                }
                else
                {
                    // If we don't, start loading the image, if it's not loading already.
                    this.mainForm.SetLoadingPicture();

                    if (!this.imageCache[CacheCurrentIndex].CallInProgress)
                    {
                        this.LoadImage(this.currentIndex);
                    }
                }
            }
        }

        public void MoveToStart()
        {
            this.SetCurrentIndex(0);
        }

        public void MoveToEnd()
        {
            this.SetCurrentIndex(this.imageList.Count - 1);
        }

        public void SaveWindowSize()
        {
            if (this.mainForm.WindowState == WindowState.Normal)
            {
                this.databaseConfig.SaveWindowSize("Main", this.mainForm);
            }
        }

		public void ChangeOrder()
		{
            if (this.imageQuery.Sort == SortType.Modified)
			{
                this.imageQuery.Sort = SortType.Random;
			}
            else if (this.imageQuery.Sort == SortType.Random)
            {
                this.imageQuery.Sort = SortType.Name;
                this.imageQuery.Ascending = true;
            }
            else
            {
                this.imageQuery.Sort = SortType.Modified;
                this.imageQuery.Ascending = false;
            }

            this.RunImageQuery(0);
		}

        public void RunSearch(string searchTerm, bool searchFileName, bool searchTags)
        {
            this.databaseConfig.SearchFileName = searchFileName;
            this.databaseConfig.SearchTags = searchTags;

            this.imageQuery.SearchFileName = searchFileName;
            this.imageQuery.SearchTags = searchTags;
            this.imageQuery.Search = searchTerm;

            this.RunImageQuery(0);
        }

		public void ClearSearch()
		{
            this.imageQuery.Search = "";
            this.imageQuery.CustomClause = "";

            this.RunImageQuery(0);
		}

        public void UpdateSettings(Settings settingsWindow)
        {
            bool updatesEnabled = settingsWindow.UpdatesEnabled;
            bool trackDuplicates = settingsWindow.TrackDuplicates;
            bool trackImageDuplicates = settingsWindow.TrackImageDuplicates;
            DirectorySet sourceDirectories = settingsWindow.SourceDirectories;
            List<string> movedDirectories = settingsWindow.MovedDirectories;
            FullscreenStart fullscreenStart = settingsWindow.FullscreenStart;
            bool overlayInfobar = settingsWindow.OverlayInfobar;

            if (this.databaseConfig.UpdatesEnabled != updatesEnabled)
            {
                this.databaseConfig.UpdatesEnabled = updatesEnabled;

                // Only check for updates in release mode.
#if !DEBUG
                if (updatesEnabled)
                {
                    // If we don't already have an update waiting to install, check for updates.
                    if (this.processDownloadsUpdates && this.databaseConfig.GetConfigString("UpdateInstallerLocation") == string.Empty)
                    {
                        this.updateDownloader = new BackgroundWorker { WorkerSupportsCancellation = true };
                        updateDownloader.DoWork += this.CheckAndDownloadUpdate;
                        updateDownloader.RunWorkerAsync();
                    }
                }
                else
                {
                    // If we have just turned off updates, cancel any pending downloads.
                    this.updateDownloader.CancelAsync();
                }
#endif
            }

            this.databaseConfig.FullscreenStartSetting = fullscreenStart;
            this.databaseConfig.OverlayInfobar = overlayInfobar;
            this.mainForm.OverlayInfobar = overlayInfobar;

            bool refreshFiles = false;
            bool refreshView = false;
            bool refreshIncludedFlag = false;

            if (!this.databaseConfig.SourceDirectories.Equals(sourceDirectories))
            {
                this.databaseConfig.SourceDirectories = sourceDirectories;
                this.movedDirectoriesToProcess = movedDirectories;
                refreshIncludedFlag = true;
                refreshFiles = true;
            }

            if (this.databaseConfig.TrackDuplicates != trackDuplicates)
            {
                if (trackDuplicates)
                {
                    this.databaseConfig.TrackDuplicates = true;
                    refreshFiles = true;
                }
                else
                {
                    this.databaseConfig.TrackDuplicates = false;
                    refreshView = true;
                }
            }

            if (this.databaseConfig.TrackImageDuplicates != trackImageDuplicates)
            {
                this.databaseConfig.TrackImageDuplicates = trackImageDuplicates;

                if (trackDuplicates)
                {
                    refreshFiles = true;
                }
            }

            if (refreshIncludedFlag)
            {
                Database.RefreshIncludedFlag(this.connection, sourceDirectories);
            }

            if (refreshFiles)
            {
                this.RefreshFiles();
            }
            else if (refreshView)
            {
                this.mainForm.UpdateInfoBar(this);
            }
        }

		public void ReloadFiles()
		{
			this.RefreshFiles();
		}

        public void RateImage(int rating)
        {
            if (this.CurrentImage != null)
            {
                Database.SetRating(this.connection, rating, this.CurrentImageId);

                this.mainForm.UpdateInfoBar(this);
            }
        }

		public void SetRatingView(int rating)
		{
            this.imageQuery.MinRating = rating;

            this.RunImageQuery(0);
		}

		public void ToggleUntagged()
		{
            this.imageQuery.UntaggedOnly = !this.imageQuery.UntaggedOnly;

            this.RunImageQuery(0);
		}

        public void MoveVisibleFiles()
        {
            System.Windows.Forms.FolderBrowserDialog folderDialog = new System.Windows.Forms.FolderBrowserDialog();
            folderDialog.Description = "Choose where to move the images.";
            if (this.databaseConfig.SourceDirectories.BaseDirectories.Count == 1)
            {
                folderDialog.SelectedPath = this.databaseConfig.SourceDirectories.BaseDirectories[0].Path;
            }

            if (folderDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
            {
                return;
            }

            string targetFolder = folderDialog.SelectedPath;
            bool targetFolderIncluded = this.databaseConfig.SourceDirectories.PathIsIncluded(targetFolder);

            if (!targetFolderIncluded && MessageBox.Show("The directory you specified is not included in your image collection. Do you still want to move the files there?", "Confirm Move", MessageBoxButton.YesNo) == MessageBoxResult.No)
            {
                return;
            }

            int numFilesMoved = 0;
            List<string> filesFailed = new List<string>();

            using (SQLiteTransaction transaction = this.connection.BeginTransaction())
            {
                SQLiteCommand updateImageName = new SQLiteCommand("UPDATE images SET name = ? WHERE id = ?", this.connection);
                SQLiteParameter updateNameParam = updateImageName.Parameters.Add("name", System.Data.DbType.String);
                SQLiteParameter updateIdParam = updateImageName.Parameters.Add("id", System.Data.DbType.Int32);

                SQLiteCommand disincludeImage = new SQLiteCommand("UPDATE images SET included = ? WHERE id = ?", this.connection);
                SQLiteParameter disincludeParam = disincludeImage.Parameters.Add("included", System.Data.DbType.Boolean);
                SQLiteParameter disincludeIdParam = disincludeImage.Parameters.Add("id", System.Data.DbType.Int32);
                disincludeParam.Value = false;

                SQLiteCommand getImageName = new SQLiteCommand("SELECT name FROM images WHERE id = ?", this.connection);
                SQLiteParameter getIdParam = getImageName.Parameters.Add("id", System.Data.DbType.Int32);

                foreach (int imageId in this.imageList)
                {
                    getIdParam.Value = imageId;
                    string oldPath;

                    using (SQLiteDataReader reader = getImageName.ExecuteReader())
                    {
                        reader.Read();
                        oldPath = reader.GetString("name");
                    }

                    if (Path.GetDirectoryName(oldPath).ToLowerInvariant() != targetFolder.ToLowerInvariant())
                    {
                        string targetFile = Path.Combine(targetFolder, Path.GetFileName(oldPath));

                        try
                        {
                            if (!File.Exists(targetFile))
                            {
                                File.Move(oldPath, targetFile);
                            }
                            else
                            {
                                targetFile = Path.Combine(targetFolder, Path.GetFileNameWithoutExtension(oldPath) + "_" + Utilities.GetRandomString(5) + Path.GetExtension(oldPath));

                                File.Move(oldPath, targetFile);
                            }

                            updateNameParam.Value = targetFile;
                            updateIdParam.Value = imageId;
                            updateImageName.ExecuteNonQuery();

                            if (!targetFolderIncluded)
                            {
                                disincludeIdParam.Value = imageId;
                                disincludeImage.ExecuteNonQuery();
                            }

                            numFilesMoved++;
                        }
                        catch (FileNotFoundException)
                        {
                            // Ignore, if the file is not there to move, we don't care.
                        }
                        catch (IOException)
                        {
                            filesFailed.Add(oldPath);
                        }
                    }
                }

                transaction.Commit();
            }

            StringBuilder messageBuilder = new StringBuilder();

            if (numFilesMoved > 0)
            {
                messageBuilder.Append(numFilesMoved + " file(s) moved to " + targetFolder + " successfully.");
            }
            else
            {
                messageBuilder.Append("No files moved.");
            }

            if (filesFailed.Count > 0)
            {
                messageBuilder.Append(Environment.NewLine);
                messageBuilder.Append(Environment.NewLine);
                messageBuilder.Append("The following files were locked or in use and could not be moved:");

                foreach (string failedFile in filesFailed)
                {
                    messageBuilder.Append(Environment.NewLine);
                    messageBuilder.Append(failedFile);
                }
            }

            MessageBox.Show(messageBuilder.ToString());

            if (targetFolderIncluded)
            {
                this.mainForm.UpdateInfoBar(this);
            }
            else
            {
                this.RunImageQuery(0);
            }
        }

        public void DeleteCurrentImage()
        {
            if (this.CurrentImage != null)
            {
                if (MessageBox.Show("Really delete?", "Confirm delete", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
                {
                    try
                    {
                        int imageId = this.CurrentImageId;
                        string imageName = Database.GetImageName(this.connection, imageId);

                        // Delete file
                        File.Delete(imageName);

                        bool indexChanged = false;

                        // Remove image from internal structures
                        using (SQLiteTransaction transaction = this.connection.BeginTransaction())
                        {
                            SQLiteCommand deleteImage = new SQLiteCommand("DELETE FROM images WHERE id = " + imageId, this.connection);
                            deleteImage.ExecuteNonQuery();

                            SQLiteCommand deleteImageSummary = new SQLiteCommand("DELETE FROM summaries WHERE imageId = " + imageId, this.connection);
                            deleteImageSummary.ExecuteNonQuery();

                            transaction.Commit();
                        }

                        this.imageList.RemoveAt(this.currentIndex);

                        if (this.currentIndex == this.imageList.Count)
                        {
                            this.currentIndex--;
                            indexChanged = true;
                        }

                        this.mainForm.RemoveFromCache(this.imageCache[CacheCurrentIndex].UIImage);

                        if (indexChanged)
                        {
                            // If the index changed, we deleted the last image in the list, so we move up
                            //  the images behind us.
                            for (int i = NumImagesToCacheAhead; i > 0; i--)
                            {
                                this.imageCache[i] = this.imageCache[i - 1];
                            }

                            this.imageCache[0] = new CachedImage();
                        }
                        else
                        {
                            // If the index did not change, we did not delete the last image on the list,
                            //  so we move back all the images ahead of us.
                            for (int i = NumImagesToCacheAhead; i < NumImagesToCacheAhead * 2; i++)
                            {
                                this.imageCache[i] = this.imageCache[i + 1];
                            }

                            this.imageCache[NumImagesToCacheAhead * 2] = new CachedImage();
                        }

                        // Increment list version so in-progress calls are ignored when they complete.
                        this.imageListVersion++;

                        // Clear out loading flags, recalculate duplicates for cached images.
                        foreach (CachedImage cachedImage in this.imageCache)
                        {
                            cachedImage.CallInProgress = false;

                            if (this.databaseConfig.TrackDuplicates && cachedImage.Image != null)
                            {
                                cachedImage.Image.PopulateDuplicates(this.connection);
                            }
                        }

                        // Show our new cached image.
                        this.mainForm.SetPicture(this.imageCache[CacheCurrentIndex]);

                        // Start loading new images if we need to.
                        if (this.imageList.Count > 0)
                        {
                            this.StartLoadingAdditionalImages();
                        }
                    }
                    catch (IOException)
                    {
                        HandleFileException("delete");
                    }
                    catch (UnauthorizedAccessException)
                    {
                        HandleFileException("delete");
                    }
                }
            }
        }

        public void EditCurrentImage()
        {
            if (this.CurrentImage != null)
            {
                string imagePath = Database.GetImageName(this.connection, this.CurrentImageId);

                RegistryKey key = Registry.ClassesRoot.OpenSubKey(@"SystemFileAssociations\image\shell\edit\command");

                if (key == null)
                {
                    MessageBox.Show("Could not find image editor program.");
                    return;
                }

                string keyValue = key.GetValue("").ToString();

                string processStartCommand = keyValue.Replace("%1", imagePath);

                ProcessStartInfo processStartInfo = new ProcessStartInfo { UseShellExecute = false, FileName = processStartCommand };
                Process process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };

                // Begin execution
                process.Exited += (object sender, EventArgs e) =>
                {
                    Process editProcess = sender as Process;

                    Action<string> dispatcherMethod = new Action<string>((string processFileName) =>
                    {
                        foreach (CachedImage cachedImage in this.imageCache)
                        {
                            if (cachedImage.Image != null)
                            {
                                string cachedImageName = Database.GetImageName(this.connection, cachedImage.Image.ImageId);
                                if (processFileName.ToLowerInvariant().Contains(cachedImageName.ToLowerInvariant()))
                                {
                                    this.SetCurrentIndex(this.currentIndex);
                                }
                            }
                        }
                    });

                    this.mainForm.Dispatcher.Invoke(dispatcherMethod, editProcess.StartInfo.FileName);
                };

                process.Start();
            }
        }

        private void EditorFinished(object sender, EventArgs e)
        {
            Process editProcess = sender as Process;

            Debug.WriteLine(editProcess.StartInfo.FileName);
        }

        public void CleanVisibleItems(List<Regex> importRegList, bool deleteDuplicates)
        {
            var imagesToRemove = new List<int>();

            Dictionary<int, List<string>> currentTags = Database.GetMultipleTags(this.connection, this.imageList);

            using (SQLiteTransaction transaction = this.connection.BeginTransaction())
            {

                foreach (int imageId in this.imageList)
                {
                    string fileName = Database.GetImageName(this.connection, imageId);

                    if (File.Exists(fileName))
                    {
                        // See if the image needs to be deleted
                        if (deleteDuplicates)
                        {
                            List<int> exactDuplicates = DuplicateUtils.FindExactDuplicates(this.connection, imageId);

                            foreach (int duplicateImageId in exactDuplicates)
                            {
                                // If there's an exact duplicate not visible, we delete the item
                                if (!this.imageList.Contains(duplicateImageId))
                                {
                                    try
                                    {
                                        try
                                        {
                                            File.Delete(fileName);
                                        }
                                        catch (FileNotFoundException)
                                        {
                                            // If it's already gone we just go ahead and remove the file metadata.
                                        }

                                        imagesToRemove.Add(imageId);
                                    }
                                    catch (IOException)
                                    {
                                        // Eat exception, we want to fail silently in bulk operations.
                                    }
                                    catch (UnauthorizedAccessException)
                                    {
                                        // Eat exception, we want to fail silently in bulk operations.
                                    }
                                }
                            }
                        }

                        string localFileName = Path.GetFileName(fileName);

                        foreach (Regex importExpression in importRegList)
                        {
                            Match m = importExpression.Match(localFileName);

                            if (m.Success)
                            {
                                if (m.Groups["filename"].Value != "")
                                {
                                    string newLocalName = m.Groups["filename"].Value;

                                    if (localFileName != newLocalName)
                                    {
                                        string newNameWithDir = Path.Combine(Path.GetDirectoryName(fileName), newLocalName);

                                        if (!File.Exists(newNameWithDir))
                                        {
                                            try
                                            {
                                                File.Move(fileName, newNameWithDir);

                                                Database.SetImageName(this.connection, imageId, newNameWithDir);
                                            }
                                            catch (IOException)
                                            {
                                                HandleFileException("move");
                                            }
                                            catch (UnauthorizedAccessException)
                                            {
                                                HandleFileException("move");
                                            }
                                        }
                                    }
                                }

                                if (m.Groups["artist"].Value != "")
                                {
                                    string newTag = m.Groups["artist"].Value.ToLowerInvariant();

                                    if (!currentTags[imageId].Contains(newTag))
                                    {
                                        currentTags[imageId].Add(newTag);

                                        Database.SetTags(this.connection, currentTags[imageId], imageId, this.databaseConfig.TagDependencies);
                                    }
                                }

                                break;
                            }
                        }
                    }
                }

                transaction.Commit();
            }



            if (deleteDuplicates && imagesToRemove.Count > 0)
            {
                this.RemoveDeletedImageData(imagesToRemove);
            }
            else
            {
                this.SetCurrentIndex(this.currentIndex);
            }
        }

		public void AddTagToVisible(string tags)
		{
			string firstTag = GetFirstTag(tags);

			if (!firstTag.Contains("|") && firstTag != "")
			{
                Database.AddTagToImages(this.connection, firstTag, this.imageList, this.databaseConfig.TagDependencies);

                this.mainForm.UpdateInfoBar(this);
			}
		}

		public void RemoveTagFromVisible(string tags)
		{
			string firstTag = GetFirstTag(tags);

			if (!firstTag.Contains("|") && firstTag != "")
			{
                Database.RemoveTagFromImages(this.connection, firstTag, this.imageList, this.databaseConfig.TagDependencies);

                this.mainForm.UpdateInfoBar(this);
			}
		}

        public void ShowImageList(int listId)
        {
            this.SetImageList(listId);

            this.RunImageQuery(0);
        }

        /// <summary>
        /// Called when files have been deleted and we need to update our data structures.
        /// </summary>
        /// <param name="deletedImages">The images that have been deleted.</param>
        public void RemoveDeletedImageData(List<int> deletedImages)
        {
            if (deletedImages.Count > 0)
            {
                using (SQLiteTransaction transaction = this.connection.BeginTransaction())
                {
                    var idParameter = new SQLiteParameter();
                    SQLiteCommand removeImage = new SQLiteCommand(this.connection);
                    removeImage.CommandText = "DELETE FROM images WHERE id = ?";
                    removeImage.Parameters.Add(idParameter);

                    var idSummaryParameter = new SQLiteParameter();
                    SQLiteCommand removeImageSummary = new SQLiteCommand(this.connection);
                    removeImageSummary.CommandText = "DELETE FROM summaries WHERE imageId = ?";
                    removeImageSummary.Parameters.Add(idSummaryParameter);

                    foreach (int imageId in deletedImages)
                    {
                        idParameter.Value = imageId;
                        removeImage.ExecuteNonQuery();

                        idSummaryParameter.Value = imageId;
                        removeImageSummary.ExecuteNonQuery();

                        int indexOfImage = this.imageList.IndexOf(imageId);

                        if (indexOfImage >= 0)
                        {
                            this.imageList.RemoveAt(indexOfImage);

                            if (indexOfImage < this.currentIndex)
                            {
                                this.currentIndex--;
                            }
                        }
                    }

                    transaction.Commit();
                }

                this.CurrentImage = null;

                if (this.currentIndex == this.imageList.Count)
                {
                    this.currentIndex--;
                }

                this.SetCurrentIndex(this.currentIndex);
            }
        }

        /// <summary>
        /// Runs the current image query, and if we have a new image list, make it current and change
        /// the view to the given index.
        /// </summary>
        /// <param name="startingIndex">The index to start at if we get a new image list.</param>
        public void RunImageQuery(int startingIndex)
        {
            List<int> newImageList = this.imageQuery.GetImageList(this.connection);

            if (newImageList != null)
            {
                this.imageList = newImageList;
                this.SetCurrentIndex(startingIndex);
            }
        }

        public void PromptToApplyUpdate()
        {
#if !DEBUG

            // If updates are enabled, and we are the last process instance, prompt to apply the update.
            if (this.databaseConfig.UpdatesEnabled && Utilities.CurrentProcessInstances == 1)
            {
                string installerPath = this.databaseConfig.GetConfigString("UpdateInstallerLocation");

                if (installerPath != string.Empty)
                {
                    // An update is ready, to give a prompt to apply it.
                    ApplyUpdateConfirmation updateConfirmation = new ApplyUpdateConfirmation(this);
                    updateConfirmation.Owner = this.mainForm;
                    updateConfirmation.ShowDialog();
                }
                else
                {
                    if (this.updateDownloader != null)
                    {
                        this.updateDownloader.CancelAsync();
                    }
                }
            }
#endif
        }

        public void ApplyUpdate()
        {
            // Re-check the process count in case another one was opened while the prompt was active.
            if (Utilities.CurrentProcessInstances == 1)
            {
                string installerPath = this.databaseConfig.GetConfigString("UpdateInstallerLocation");

                this.databaseConfig.SetConfigValue("UpdateInProgress", true);

                Process installerProcess = new Process();
                installerProcess.StartInfo = new ProcessStartInfo { FileName = installerPath, Arguments = "/silent /noicons" };
                installerProcess.Start();

                // We want to make sure the program is gone when the update installer runs.
                Environment.Exit(0);
            }
        }

		#endregion

		#region Private Methods

        private void HandlePendingUpdate(SQLiteConnection connection)
        {
            bool updateInProgress = DatabaseConfigAccess.GetConfigBool("UpdateInProgress", connection);
            string targetUpdateVersion = DatabaseConfigAccess.GetConfigString("UpdateVersion", connection);

            if (updateInProgress)
            {
                DatabaseConfigAccess.SetConfigValue("UpdateInProgress", false, connection);

                // This flag signifies KIV is being run by the installer after an update.
                // In this case we just report success and exit.
                if (targetUpdateVersion == Utilities.CurrentVersion)
                {
                    MessageBox.Show("Keyboard Image Viewer has been successfully updated.");
                    Environment.Exit(0);
                }

                // If the target version is different from the currently running version,
                // this means the attempted upgrade failed. We give an error message but
                // continue with the program.
                MessageBox.Show("The update was not applied. If you did not cancel it, try installing it manually.");
            }
        }

        private void CheckUpdates()
        {
            // Only check for updates in release mode.
#if !DEBUG
            if (Utilities.CurrentProcessInstances > 1)
            {
                this.processDownloadsUpdates = false;

                return;
            }

            if (!this.databaseConfig.UpdatesEnabled)
            {
                // On a program restart, if updates are disabled, clean any pending installers.
                this.databaseConfig.SetConfigValue("UpdateInstallerLocation", string.Empty);

                if (Directory.Exists(Utilities.UpdatesFolder))
                {
                    Directory.Delete(Utilities.UpdatesFolder, true);
                }

                return;
            }

            string targetUpdateVersion = this.databaseConfig.GetConfigString("UpdateVersion");

            if (targetUpdateVersion != string.Empty)
            {
                // We've installed the update, so clean up the installer and reset the update flags.
                if (Directory.Exists(Utilities.UpdatesFolder))
                {
                    Directory.Delete(Utilities.UpdatesFolder, true);
                }

                using (SQLiteTransaction transaction = this.connection.BeginTransaction())
                {
                    this.databaseConfig.SetConfigValue("UpdateVersion", string.Empty);
                    this.databaseConfig.SetConfigValue("UpdateInstallerLocation", string.Empty);
                    this.databaseConfig.SetConfigValue("UpdateChangelogLocation", string.Empty);

                    transaction.Commit();
                }
            }

            this.updateDownloader = new BackgroundWorker { WorkerSupportsCancellation = true };
            updateDownloader.DoWork += this.CheckAndDownloadUpdate;
            updateDownloader.RunWorkerAsync();
#endif
        }

        private void CheckAndDownloadUpdate(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker updateDownloader = sender as BackgroundWorker;

            try
            {
                XDocument document = XDocument.Load(Utilities.UpdateInfoUrl);
                XElement root = document.Root;
                XElement latestElement = root.Element("Latest");
                XElement downloadElement = root.Element("DownloadLocation");
                XElement changelogLinkElement = root.Element("ChangelogLocation");

                if (latestElement == null || downloadElement == null || changelogLinkElement == null)
                {
                    return;
                }

                string updateVersion = latestElement.Value;

                SQLiteConnection workerConnection = new SQLiteConnection(Database.ConnectionString);
                workerConnection.Open();

                ViewerDatabaseConfig config = new ViewerDatabaseConfig(workerConnection, false);

                if (Utilities.CompareVersions(updateVersion, Utilities.CurrentVersion) > 0)
                {
                    // If we have not finished the download update yet, start/resume the download.
                    if (config.GetConfigString("UpdateInstallerLocation") == string.Empty)
                    {
                        string downloadLocation = downloadElement.Value;
                        string changelogLink = changelogLinkElement.Value;
                        string installerFileName = Path.GetFileName(downloadLocation);
                        string installerFilePath = Path.Combine(Utilities.UpdatesFolder, installerFileName);

                        Stream responseStream = null;
                        FileStream fileStream = null;

                        try
                        {
                            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(downloadLocation);
                            
                            if (File.Exists(installerFilePath))
                            {
                                FileInfo fileInfo = new FileInfo(installerFilePath);

                                request.AddRange((int)fileInfo.Length);

                                fileStream = new FileStream(installerFilePath, FileMode.Append, FileAccess.Write, FileShare.None);
                            }
                            else
                            {
                                fileStream = new FileStream(installerFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
                            }

                            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                            responseStream = response.GetResponseStream();

                            byte[] downloadBuffer = new byte[2048];
                            int bytesRead = 0;

                            while ((bytesRead = responseStream.Read(downloadBuffer, 0, downloadBuffer.Length)) > 0 && !updateDownloader.CancellationPending)
                            {
                                fileStream.Write(downloadBuffer, 0, bytesRead);
                            }

                            if (bytesRead == 0)
                            {
                                using (SQLiteTransaction transaction = workerConnection.BeginTransaction())
                                {
                                    config.SetConfigValue("UpdateVersion", updateVersion);
                                    config.SetConfigValue("UpdateInstallerLocation", installerFilePath);
                                    config.SetConfigValue("UpdateChangelogLocation", changelogLink);

                                    transaction.Commit();
                                }
                            }
                        }
                        finally
                        {
                            if (responseStream != null)
                            {
                                responseStream.Close();
                            }

                            if (fileStream != null)
                            {
                                fileStream.Close();
                            }
                        }
                    }
                }
            }
            catch (WebException)
            {
                // Fail silently
            }
            catch (XmlException)
            {
                // Fail silently
            }
        }

        private void SetCurrentIndex(int index)
        {
            if (index >= 0 && index < this.imageList.Count)
            {
                this.currentIndex = index;

                this.clearCacheWhenLoadCompletes = true;
                //this.mainForm.ClearCache();

                this.imageCache = new CachedImage[this.imageCache.Length];
                for (int i = 0; i < this.imageCache.Length; i++)
                {
                    this.imageCache[i] = new CachedImage();
                }

                this.imageListVersion++;

                this.LoadImage(this.currentIndex);
            }
            else
            {
                this.mainForm.SetPicture(null);
            }
        }

        private void LoadImage(int index)
        {
            BackgroundWorker imageLoader = new BackgroundWorker();
            double aspectRatio = this.mainForm.ImageAreaAspectRatio;
            int imageId = this.imageList[index];
            int startingImageListVersion = this.imageListVersion;

            this.imageCache[CacheCurrentIndex + index - this.currentIndex].CallInProgress = true;

            imageLoader.DoWork += delegate(object sender, DoWorkEventArgs args)
            {
                System.Threading.Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;

                SQLiteConnection workerConnection = new SQLiteConnection(Database.ConnectionString);
                workerConnection.Open();

                BackgroundWorker theWorker = sender as BackgroundWorker;

                args.Result = new ViewerImage(imageId, aspectRatio, this, workerConnection);
            };

            imageLoader.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs args)
            {
                // We've since thrown away the image list we started on, throw it away.
                if (startingImageListVersion != this.imageListVersion)
                {
                    return;
                }

                if (this.clearCacheWhenLoadCompletes)
                {
                    this.mainForm.ClearCache();

                    this.clearCacheWhenLoadCompletes = false;
                }

                int indexOffset = index - this.currentIndex;

                // The image we've got isn't inside the cache window, throw it away.
                if (Math.Abs(indexOffset) > NumImagesToCacheAhead)
                {
                    return;
                }

                int cacheIndex = CacheCurrentIndex + indexOffset;

                // We already have an image in cache for this index, throw it away.
                if (this.imageCache[cacheIndex].Image != null)
                {
                    return;
                }

                ViewerImage finishedImage = args.Result as ViewerImage;

                // Populate the image cache with the built ViewerImage.
                this.imageCache[cacheIndex].Image = finishedImage;
                this.imageCache[cacheIndex].UIImage = this.mainForm.AddToCache(finishedImage);
                if (finishedImage.Rotation != 0)
                {
                    this.imageCache[cacheIndex].UIImage.LayoutTransform = new RotateTransform(finishedImage.Rotation);
                }

                // If we came back with the current image, display it.
                if (indexOffset == 0)
                {
                    this.mainForm.SetPicture(this.imageCache[cacheIndex]);
                }

                // Start loading more images in the background if needed.
                this.StartLoadingAdditionalImages();
            };

            imageLoader.RunWorkerAsync();
        }

        private void StartLoadingAdditionalImages()
        {
            if (this.imageCache[CacheCurrentIndex].Image == null && !this.imageCache[CacheCurrentIndex].CallInProgress)
            {
                this.LoadImage(this.currentIndex);
            }

            int leftBoundIndex = Math.Max(0, NumImagesToCacheAhead - this.currentIndex);
            int rightBoundIndex = Math.Min(this.imageCache.Length - 1, NumImagesToCacheAhead + this.imageList.Count - this.currentIndex - 1);

            int furthestLoaded = -1;
            int furthestPossible = 0;

            if (this.imageCache[CacheCurrentIndex].Image != null)
            {
                furthestLoaded = 0;
            }

            for (int i = 1; i <= NumImagesToCacheAhead; i++)
            {
                bool rightIndexInBounds = CacheCurrentIndex + i <= rightBoundIndex;
                bool leftIndexInBounds = CacheCurrentIndex - i >= leftBoundIndex;

                if (rightIndexInBounds || leftIndexInBounds)
                {
                    furthestPossible = i;
                }
                else
                {
                    break;
                }

                bool rightImage = this.imageCache[CacheCurrentIndex + i].Image != null;
                bool leftImage = this.imageCache[CacheCurrentIndex - i].Image != null;

                if ((rightImage || !rightIndexInBounds) && (leftImage || !leftIndexInBounds))
                {
                    if (furthestLoaded == i - 1)
                    {
                        furthestLoaded++;
                    }
                }
            }

            if (furthestLoaded < furthestPossible && furthestLoaded >= 0)
            {
                int indexOffsetToLoad = furthestLoaded + 1;

                if (CacheCurrentIndex - indexOffsetToLoad >= leftBoundIndex &&
                    !this.imageCache[CacheCurrentIndex - indexOffsetToLoad].CallInProgress)
                {
                    this.LoadImage(this.currentIndex - indexOffsetToLoad);
                }

                if (CacheCurrentIndex + indexOffsetToLoad <= rightBoundIndex &&
                    !this.imageCache[CacheCurrentIndex + indexOffsetToLoad].CallInProgress)
                {
                    this.LoadImage(this.currentIndex + indexOffsetToLoad);
                }
            }
        }

        private void RefreshFiles()
        {
            this.mainForm.AllowInput = false;

            List<FileInfo> fileList = this.GetAllImages(this.databaseConfig.SourceDirectories);

            List<OrphanedData> orphanedImages = new List<OrphanedData>();

            fileList.Sort(Utilities.FileInfoNameComparer);

            SQLiteCommand getIncludedItems = new SQLiteCommand("SELECT id, name, lastWriteTime, hash, images.lastRefreshDate AS imagesLastRefresh, summaries.lastRefreshDate AS summariesLastRefresh, ineligible, tags, rating FROM images LEFT JOIN summaries ON images.id = summaries.imageId WHERE included ORDER BY lower(name) ASC", this.connection);
            SQLiteDataReader reader = getIncludedItems.ExecuteReader();

            int numImagesToHash = 0;
            int numImagesToSummarize = 0;

            long newImageId = Database.GetLastInsertId(this.connection) + 1;

            using (SQLiteTransaction transaction = this.connection.BeginTransaction())
            {
                var updateWriteTimeTime = new SQLiteParameter();
                var updateWriteTimeId = new SQLiteParameter();

                SQLiteCommand updateWriteTime = new SQLiteCommand(this.connection);
                updateWriteTime.CommandText = "UPDATE images SET lastWriteTime = ? WHERE id = ?";
                updateWriteTime.Parameters.Add(updateWriteTimeTime);
                updateWriteTime.Parameters.Add(updateWriteTimeId);

                var updateNameName = new SQLiteParameter();
                var updateNameId = new SQLiteParameter();

                SQLiteCommand updateName = new SQLiteCommand(this.connection);
                updateName.CommandText = "UPDATE images SET name = ? WHERE id = ?";
                updateName.Parameters.Add(updateNameName);
                updateName.Parameters.Add(updateNameId);

                var insertNewImageId = new SQLiteParameter();
                var insertNewImageName = new SQLiteParameter();
                var insertNewImageLastWriteTime = new SQLiteParameter();
                var insertNewImageIncluded = new SQLiteParameter();

                SQLiteCommand insertNewImage = new SQLiteCommand(this.connection);
                insertNewImage.CommandText = "INSERT INTO images (id, name, lastWriteTime, included) VALUES (?, ?, ?, ?)";
                insertNewImage.Parameters.Add(insertNewImageId);
                insertNewImage.Parameters.Add(insertNewImageName);
                insertNewImage.Parameters.Add(insertNewImageLastWriteTime);
                insertNewImage.Parameters.Add(insertNewImageIncluded);

                insertNewImageIncluded.Value = true;

                var deleteOldImageId = new SQLiteParameter();
                SQLiteCommand deleteOldImage = new SQLiteCommand(this.connection);
                deleteOldImage.CommandText = "DELETE FROM images WHERE id = ?";
                deleteOldImage.Parameters.Add(deleteOldImageId);

                var deleteOldImageSummaryId = new SQLiteParameter();
                SQLiteCommand deleteOldImageSummary = new SQLiteCommand(this.connection);
                deleteOldImageSummary.CommandText = "DELETE FROM summaries WHERE imageId = ?";
                deleteOldImageSummary.Parameters.Add(deleteOldImageSummaryId);

                int i = 0;
                bool rowRead;

                while ((rowRead = reader.Read()) || i < fileList.Count)
                {
                    while (i < fileList.Count && (!rowRead || String.Compare(fileList[i].FullName.ToLowerInvariant(), reader.GetString("name").ToLowerInvariant()) < 0))
                    {
                        // Add new image to main table
                        insertNewImageId.Value = newImageId;
                        insertNewImageName.Value = fileList[i].FullName;
                        insertNewImageLastWriteTime.Value = fileList[i].LastWriteTime;

                        insertNewImage.ExecuteNonQuery();

                        // Schedule processing for new image
                        if (this.databaseConfig.TrackDuplicates)
                        {
                            if (this.databaseConfig.TrackImageDuplicates)
                            {
                                numImagesToHash++;
                                numImagesToSummarize++;

                                this.imagesToProcess.Enqueue(new SummaryAndHashTask(newImageId, fileList[i].FullName, fileList[i].LastWriteTime, true));
                            }
                            else
                            {
                                numImagesToHash++;

                                this.imagesToProcess.Enqueue(new HashTask(newImageId, fileList[i].FullName, fileList[i].LastWriteTime));
                            }
                        }

                        newImageId++;

                        i++;
                    }

                    if (!rowRead)
                    {
                        continue;
                    }

                    string databaseName = reader.GetString("name");
                    int databaseId = reader.GetInt32("id");

                    if (i >= fileList.Count || fileList[i].FullName.ToLowerInvariant() != databaseName.ToLowerInvariant())
                    {
                        // This item is orphaned, add it to the list to process later
                        OrphanedData orphanedData = new OrphanedData { Name = databaseName, ImageId = databaseId };

                        if (this.databaseConfig.TrackDuplicates && !reader.IsDBNull("hash"))
                        {
                            orphanedData.Hash = reader.GetString("hash");
                        }

                        if (!reader.IsDBNull("tags"))
                        {
                            orphanedData.Tags = reader.GetString("tags");
                        }

                        if (!reader.IsDBNull("rating"))
                        {
                            orphanedData.Rating = reader.GetInt32("rating");
                        }

                        orphanedImages.Add(orphanedData);

                        // Remove original database entry for item
                        deleteOldImageId.Value = databaseId;
                        deleteOldImage.ExecuteNonQuery();

                        deleteOldImageSummaryId.Value = databaseId;
                        deleteOldImageSummary.ExecuteNonQuery();
                    }
                    else
                    {
                        // Update LastWriteTime and ensure hash/image summary is up to date
                        if (reader.IsDBNull("lastWriteTime") || reader.GetDateTime("lastWriteTime") != fileList[i].LastWriteTime)
                        {
                            updateWriteTimeId.Value = databaseId;
                            updateWriteTimeTime.Value = fileList[i].LastWriteTime;

                            updateWriteTime.ExecuteNonQuery();
                        }

                        if (databaseName != fileList[i].FullName)
                        {
                            updateNameName.Value = fileList[i].FullName;
                            updateNameId.Value = databaseId;

                            updateName.ExecuteNonQuery();
                        }

                        if (this.databaseConfig.TrackDuplicates)
                        {
                            bool hashRefresh = false;
                            bool summaryRefresh = false;
                            bool newRow = false;

                            if (reader.IsDBNull("hash") || reader.GetDateTime("imagesLastRefresh") < fileList[i].LastWriteTime)
                            {
                                hashRefresh = true;
                                numImagesToHash++;
                            }

                            if (this.databaseConfig.TrackImageDuplicates)
                            {
                                bool ineligible = !reader.IsDBNull("ineligible") && reader.GetBoolean("ineligible");

                                if (!ineligible && (reader.IsDBNull("summariesLastRefresh") || reader.GetDateTime("summariesLastRefresh") < fileList[i].LastWriteTime))
                                {
                                    newRow = reader.IsDBNull("summariesLastRefresh");
                                    summaryRefresh = true;
                                    numImagesToSummarize++;
                                }
                            }

                            if (hashRefresh && summaryRefresh)
                            {
                                this.imagesToProcess.Enqueue(new SummaryAndHashTask(reader.GetInt32("id"), fileList[i].FullName, fileList[i].LastWriteTime, newRow));
                            }
                            else if (hashRefresh)
                            {
                                this.imagesToProcess.Enqueue(new HashTask(reader.GetInt32("id"), fileList[i].FullName, fileList[i].LastWriteTime));
                            }
                            else if (summaryRefresh)
                            {
                                this.imagesToProcess.Enqueue(new SummaryTask(reader.GetInt32("id"), fileList[i].FullName, fileList[i].LastWriteTime, newRow));
                            }
                        }

                        i++;
                    }
                }

                reader.Dispose();

                transaction.Commit();
            }

            if (numImagesToSummarize > 0)
            {
                this.chunkSize = SummaryChunkSize;
            }
            else
            {
                this.chunkSize = HashChunkSize;
            }

            if (this.imagesToProcess.Count > 0)
            {
                if (numImagesToSummarize > 0)
                {
                    this.showingProgress = this.imagesToProcess.Count >= SummaryVisibleThreshold;
                }
                else
                {
                    this.showingProgress = this.imagesToProcess.Count >= HashVisibleThreshold;
                }
            }
            else
            {
                this.showingProgress = false;
            }

            if (this.showingProgress)
            {
                this.mainForm.SetPicture(null);
                this.mainForm.ProgressBarVisible = true;
            }

            this.chunksCompleted = 0;
            this.workersCompleted = 0;
            this.numImagesToProcess = this.imagesToProcess.Count;

            int chunks = (int)Math.Ceiling(((double)this.numImagesToProcess) / this.chunkSize);
            this.numBackgroundWorkers = Math.Max(Math.Min(Environment.ProcessorCount, chunks), 1);

            for (int i = 0; i < this.numBackgroundWorkers; i++)
            {
                BackgroundWorker worker = new BackgroundWorker();
                worker.WorkerReportsProgress = true;
                worker.DoWork += new DoWorkEventHandler(this.worker_DoWork);
                worker.ProgressChanged += new ProgressChangedEventHandler(this.worker_ProgressChanged);
                worker.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs args)
                {
                    this.workersCompleted++;

                    if (this.workersCompleted >= this.numBackgroundWorkers)
                    {
                        this.FinishFilesRefresh(orphanedImages);

                        this.mainForm.AllowInput = true;
                    }
                };

                worker.RunWorkerAsync();
            }
        }

        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            SQLiteConnection workerConnection = new SQLiteConnection(Database.ConnectionString);
            workerConnection.Open();

            List<IProcessTask> taskBatch;

            do
            {
                taskBatch = new List<IProcessTask>();

                lock (this.imagesToProcessLock)
                {
                    for (int i = 0; i < this.chunkSize && this.imagesToProcess.Count > 0; i++)
                    {
                        taskBatch.Add(this.imagesToProcess.Dequeue());
                    }
                }

                if (taskBatch.Count > 0)
                {
                    for (int i = 0; i < taskBatch.Count; i++)
                    {
                        taskBatch[i].Process();
                    }

                    using (SQLiteTransaction transaction = workerConnection.BeginTransaction())
                    {
                        for (int i = 0; i < taskBatch.Count; i++)
                        {
                            taskBatch[i].Commit(workerConnection);
                        }

                        transaction.Commit();
                    }

                    worker.ReportProgress(0);
                }
            } while (taskBatch.Count > 0);
        }

        private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            this.chunksCompleted++;
            if (this.showingProgress)
            {
                this.mainForm.ReportProgress(((double)(this.chunksCompleted * this.chunkSize * 100)) / this.numImagesToProcess);
            }
        }

        private void FinishFilesRefresh(List<OrphanedData> orphanedImages)
        {
            if (this.showingProgress)
            {
                this.mainForm.ProgressBarVisible = false;
            }

            Database.ApplyOrphanedData(orphanedImages, this.connection, this.Config.TagDependencies, this.Config.TrackDuplicates);

            if (this.movedDirectoriesToProcess != null && this.movedDirectoriesToProcess.Count > 0)
            {
                List<OrphanedData> movedImages = new List<OrphanedData>();

                using (SQLiteTransaction transaction = this.connection.BeginTransaction())
                {
                    var deleteOldImageId = new SQLiteParameter();
                    SQLiteCommand deleteOldImage = new SQLiteCommand(this.connection);
                    deleteOldImage.CommandText = "DELETE FROM images WHERE id = ?";
                    deleteOldImage.Parameters.Add(deleteOldImageId);

                    var deleteOldImageSummaryId = new SQLiteParameter();
                    SQLiteCommand deleteOldImageSummary = new SQLiteCommand(this.connection);
                    deleteOldImageSummary.CommandText = "DELETE FROM summaries WHERE imageId = ?";
                    deleteOldImageSummary.Parameters.Add(deleteOldImageSummaryId);

                    foreach (string movedDirectory in this.movedDirectoriesToProcess)
                    {
                        SQLiteCommand getMissingData = new SQLiteCommand("SELECT id, name, tags, rating, hash FROM images WHERE name LIKE \"" + movedDirectory + "%\"", this.connection);
                        using (SQLiteDataReader reader = getMissingData.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                string tags = null;
                                if (!reader.IsDBNull("tags"))
                                {
                                    tags = reader.GetString("tags");
                                }
                                int rating = 0;
                                if (!reader.IsDBNull("rating"))
                                {
                                    rating = reader.GetInt32("rating");
                                }

                                string hash = null;
                                if (!reader.IsDBNull("hash"))
                                {
                                    hash = reader.GetString("hash");
                                }

                                bool hasTags = tags != null && tags.Length > 2;

                                if (hasTags || rating > 0)
                                {
                                    OrphanedData orphanedData = new OrphanedData { Name = reader.GetString("name") };

                                    if (hasTags)
                                    {
                                        orphanedData.Tags = tags;
                                    }

                                    if (rating > 0)
                                    {
                                        orphanedData.Rating = rating;
                                    }

                                    if (!string.IsNullOrEmpty(hash))
                                    {
                                        orphanedData.Hash = hash;
                                    }

                                    movedImages.Add(orphanedData);
                                }

                                int oldImageId = reader.GetInt32("id");
                                deleteOldImageId.Value = oldImageId;
                                deleteOldImage.ExecuteNonQuery();

                                deleteOldImageSummaryId.Value = oldImageId;
                                deleteOldImageSummary.ExecuteNonQuery();
                            }
                        }
                    }

                    transaction.Commit();
                }

                Database.ApplyOrphanedData(movedImages, this.connection, this.Config.TagDependencies, this.Config.TrackDuplicates);
            }

            this.imageList = this.imageQuery.GetImageList(this.connection, true);
            this.SetCurrentIndex(0);
        }

        private void SetImageList(int listId)
        {
            this.currentListId = listId;

            SQLiteCommand command = new SQLiteCommand("SELECT name, orderBy, ascending, rating, searchString, searchTags, searchFileName, untagged, customClause FROM viewLists WHERE id = " + listId, this.connection);
            using (SQLiteDataReader reader = command.ExecuteReader())
            {
                reader.Read();

                string orderBy = reader.GetString("orderBy");

                switch (orderBy)
                {
                    case "random":
                        imageQuery.Sort = SortType.Random;
                        break;
                    case "name":
                        imageQuery.Sort = SortType.Name;
                        break;
                    case "lastWriteTime":
                        imageQuery.Sort = SortType.Modified;
                        break;
                    default:
                        break;
                }

                this.imageQuery.Ascending = reader.GetBoolean("ascending");
                this.imageQuery.MinRating = reader.GetInt32("rating");
                this.imageQuery.Search = reader.GetString("searchString");
                this.imageQuery.SearchTags = reader.GetBoolean("searchTags");
                this.imageQuery.SearchFileName = reader.GetBoolean("searchFileName");
                this.imageQuery.UntaggedOnly = reader.GetBoolean("untagged");
                this.imageQuery.CustomClause = reader.GetString("customClause");
                this.imageQuery.ListName = reader.GetString("name");
            }
        }

        private List<FileInfo> GetAllImages(DirectorySet directorySet)
        {
            List<FileInfo> fileList = new List<FileInfo>();

            bool directoriesChanged = false;

            foreach (BaseDirectory baseDirectory in directorySet.BaseDirectories)
            {
                if (baseDirectory.Missing != !Directory.Exists(baseDirectory.Path))
                {
                    directoriesChanged = true;

                    baseDirectory.Missing = !Directory.Exists(baseDirectory.Path);
                }
            }

            if (directoriesChanged)
            {
                this.databaseConfig.SourceDirectories = directorySet;

                Database.RefreshIncludedFlag(this.connection, directorySet);
            }

            foreach (BaseDirectory baseDirectory in directorySet.BaseDirectories)
            {
                if (!baseDirectory.Missing)
                {
                    this.AddImages(baseDirectory, fileList, new DirectoryInfo(baseDirectory.Path));
                }
            }

            return fileList;
        }

        private void AddImages(BaseDirectory baseDirectory, List<FileInfo> fileList, DirectoryInfo currentDir)
        {
            foreach (string extension in extensions)
            {
                fileList.AddRange(currentDir.GetFiles("*." + extension));
            }

            try
            {
                DirectoryInfo[] subDirectories = currentDir.GetDirectories();

                foreach (DirectoryInfo subDirectory in subDirectories)
                {
                    if (!baseDirectory.Exclusions.Contains(subDirectory.FullName.ToLowerInvariant()))
                    {
                        this.AddImages(baseDirectory, fileList, subDirectory);
                    }
                }
            }
            catch (UnauthorizedAccessException)
            {
                // Do nothing. We won't read images from inaccessible folders.
            }
        }

		private string GetFirstTag(string tags)
		{
			string firstTag = tags;

			int commaIndex = tags.IndexOf(',');

			if (commaIndex >= 0)
			{
				firstTag = tags.Substring(0, commaIndex);
			}

			return firstTag.Trim().ToLowerInvariant();
		}

		private void HandleFileException(string action)
		{
			MessageBox.Show("Could not " + action + " file.  Make sure that it is not write-protected or in use.", "Error with file " + action);
		}

        private void HandleMoveException()
        {
            MessageBox.Show("Could not move file. Destination file already exists, or access is denied.", "Error with file move");
        }

		#endregion
	}
}
